﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Windows.Threading;
using System.Threading;
using System.Drawing;
using System.Diagnostics;
using System.Data.SqlTypes;
using System.Windows.Media.Imaging;

namespace Player
{
    public class BitmapImageArgs : EventArgs
    {
        public BitmapSource Image
        {
            get;
            set;
        }
    }

    public class WinSession : SessionBase
    {
        private ExportCdfThread m_exportCdf;
        private readonly string m_connectionString;
        private readonly CaptureContract.PixelFormat m_pixformat;
        private readonly byte[] m_palette;
        private readonly long m_start;
        private long m_end;
        private Size m_screenSize;

        public override long Start
        {
            get { return m_start; }
        }

        public override long End
        {
            get { return m_end; }
        }

        public WinSession(string connectionString, Guid guid)
        {
            m_connectionString = connectionString;
            SessionId = guid;
            m_frames.Size = ConsoleSettings.FrameBufferSizeWin;

            m_exportCdf = new ExportCdfThread(this);
            //m_exportCdf.Updated += (sender, e) => m_exportCdfProgressUpdated.Invoke(sender, e);
            m_exportCdf.Exit += (sender, e) => ExportCdfCompleted(sender, e);//m_exportCdfCompleted.Invoke(sender, e);
            m_exportCdf.Exit += (sender, e) => ExportingCDF = false;

            // Get the header record from database and initialize the attributes of this Windows session
            WindowsSessionGetHeader.Output output = WindowsSessionGetHeader.ExecuteNonQuery(m_connectionString, new WindowsSessionGetHeader.Input(SessionId, DatabaseHelper.AuditStoreDatabaseIds[0]));
            m_pixformat = (CaptureContract.PixelFormat)output.PixelFormat;
            m_palette = output.Palette;
            UtcStartTime = output.StartTime;
            // Comment this out because even in a single database case, this shows a very odd behavior.  The TimeStampOffset time is always a bit earlier than the first frame timestamp
            // So, the progress bar shows there is a period of time at the beginning of the session that we don't have any data.
            // We keep using the first frame timestamp for now.
            // m_start = output.TimeStampOffset;  
            UserName = output.User;
            MachineName = output.Machine;

            // Get the first frame of this Windows session
            bool hasData = false;
            foreach (WindowsSessionGetFirstFrameSet.Output output2 in WindowsSessionGetFirstFrameSet.Execute(
                m_connectionString,
                new WindowsSessionGetFirstFrameSet.Input(SessionId, DatabaseHelper.AuditStoreDatabaseIds[0]),
                System.Data.CommandBehavior.SequentialAccess))
            {
                StartTime = new DateTime(output2.SystemTime).ToLocalTime();
                m_start = output2.TimeStamp;
                foreach (Frame frame in LoadFrames(output2.Data))
                {
                    CachedBufferThreadCollection.Push();
                    foreach (RfbRect rect in frame.Rectangles(true))
                    {
                        m_screenSize = rect.Size;
                        hasData = true;
                        break;
                    }
                    CachedBufferThreadCollection.Pop();
                    break;
                }
                break;
            }

            if (!hasData)
            {
                return;
            }

            // Get the last frame of this Windows session
            Frame lastFrame = null;
            foreach (WindowsSessionGetLastFrameSet.Output output3 in WindowsSessionGetLastFrameSet.Execute(
                m_connectionString,
                new WindowsSessionGetLastFrameSet.Input(SessionId, DatabaseHelper.AuditStoreDatabaseIds[DatabaseHelper.AuditStoreDatabaseIds.Count - 1]),
                System.Data.CommandBehavior.SequentialAccess))
            {
                foreach (Frame frame in LoadFrames(output3.Data))
                {
                    lastFrame = frame;
                }
                break;
            }

            Debug.Assert(lastFrame != null);
            m_end = lastFrame.TimeStamp;
            EndTime = lastFrame.SystemTime.ToLocalTime();
            Engine = CreateRenderEngine();

            Engine.ImageSourceUpdated += new EventHandler(en_ImageSourceUpdated);
        }

        void en_ImageSourceUpdated(object sender, EventArgs e)
        {
            Image = Engine.Image;
        }

        public override IEnumerable<Frame> LoadFrames(long startTimeStamp)
        {
            Log.Verbose("Timestamp: {0}", startTimeStamp);
            int start = DatabaseHelper.GetStartDatabaseIdForWindowsSession(startTimeStamp);

            for (int i = start; i < DatabaseHelper.AuditStoreDatabaseIds.Count; i++)
            {
                int databaseId = DatabaseHelper.AuditStoreDatabaseIds[i];
                foreach (WindowsSessionGetFrameSets.Output output in WindowsSessionGetFrameSets.Execute(
                                                                            m_connectionString,
                                                                            new WindowsSessionGetFrameSets.Input(SessionId, databaseId, startTimeStamp),
                                                                            System.Data.CommandBehavior.SequentialAccess))
                {
                    foreach (Frame frame in LoadFrames(output.Data))
                    {
                        if (frame.TimeStamp <= End) yield return frame;
                    }
                }
            }
        }

        /// <summary>
        /// A set of frames are grouped into frameset.
        /// Each frameset is stored in one single row independently.
        /// Frameset always starts with an i-frame and then a bunch of p-frames
        /// or i-frames following.
        /// 
        /// This method returns all the frames inside the framesets, which contains
        /// one or more than one frame with the timestamp greater than the specified
        /// value.  So, some of the returned frames may have timestamp smaller than
        /// the specified value because those are stored together with another frame,
        /// which has the timestamp greater than the specified value.
        /// 
        /// The frames with timestamp smaller than the specified value is still useful
        /// to the caller because the caller may needs it to rebuild the whole screen.
        /// The caller needs to find the latest i-frame right before the specified
        /// timestamp value in order to rebuild the correct image
        /// </summary>
        /// <param name="frameSetData"></param>
        /// <returns></returns>
        private IEnumerable<Frame> LoadFrames(SqlBytes frameSetData)
        {
            // Use StreamReader from CFW because BinaryReader provided by .NET doesn't allow us to set endianness
            StreamReader frameSetReader = new StreamReader(frameSetData.Stream, false);

            while (!frameSetReader.IsAtEnd())
            {
                // a block has time stamp, type  then compressed bits
                Debug.Assert(frameSetReader != null);
                DateTime st = new DateTime(frameSetReader.GetInt64());
                long ts = frameSetReader.GetInt64();
                byte type = frameSetReader.GetByte();
                byte[] rfbbuff = frameSetReader.GetBytes();
                Frame frame = new Frame(rfbbuff, true, CaptureContract.GetBytesPerPixel(m_pixformat));
                frame.SystemTime = st;
                frame.TimeStamp = ts;
                frame.Type = type;
                yield return frame;
            }
        }

        public override IEnumerable<Frame> SearchFrames(long timeStamp, string[] searchWords)
        {
            return null;
        }

        public override IEnumerable<EventItem> LoadEvents()
        {
            // Get all events
            IEnumerable<WindowsSessionGetEvents.Output> outputs = new List<WindowsSessionGetEvents.Output>();
            foreach (var databaseId in DatabaseHelper.AuditStoreDatabaseIds)
            {
                outputs = outputs.Concat(WindowsSessionGetEvents.Execute(m_connectionString, new WindowsSessionGetEvents.Input(SessionId, databaseId)));
            }

            return outputs.Where(i => i.Event != WashEventType.ConsoleText && i.TimeStamp <= End)
                .Select(i => Convert(i));
        }

        #region LoadEvents helpers

        private EventItem Convert(WindowsSessionGetEvents.Output input)
        {
            //return m_eventItemConverter.Convert(input);
            return null;
        }

        #endregion

        public override RenderEngine CreateRenderEngine()
        {
            return new RenderEngine(m_screenSize, System.Windows.Media.PixelFormats.Pbgra32, m_palette, m_pixformat);
        }

        public void ExportCdf(string filename)
        {
            Log.Verbose("Filename: {0}", filename);
            m_exportCdf.Stop();
            ExportingCDF = true;
            m_exportCdf.Start(Dispatcher.CurrentDispatcher, filename);
        }

        public void StopExportCdf()
        {
            m_exportCdf.Stop();
            Log.Verbose("Export CDF stopped");
        }

        private long m_framesetHeaderWriterPosition = 0;
        private int m_framesetCount = 0;

        public void WriteCdfHeader(BinaryWriter writer)
        {
            //writer.Write(CdfSession.EXPORT_MAGIC);
            //writer.Write(CdfSession.EXPORT_VERSION);
            //writer.Write(SessionId.ToByteArray());
            //writer.Write(MachineName);
            //writer.Write(StartTime.Ticks);
            //writer.Write(m_start);
            //writer.Write(UserName);
            //writer.Write("");                  // TBD: Client name
            //writer.Write(EndTime.Ticks);
            //writer.Write((byte)m_pixformat);
            //if (m_palette != null)
            //{
            //    writer.Write(m_palette.Length);
            //    writer.Write(m_palette);
            //}
            //else
            //{
            //    writer.Write((int)0);
            //}
        }

        public void WriteCdfEvents(BinaryWriter writer)
        {
            List<WindowsSessionGetEvents.Output> events = new List<WindowsSessionGetEvents.Output>();
            foreach (var databaseId in DatabaseHelper.AuditStoreDatabaseIds)
            {
                events.AddRange(WindowsSessionGetEvents.Execute(m_connectionString, new WindowsSessionGetEvents.Input(SessionId, databaseId)));
            }

            writer.Write(events.Count);
            EventItem item = new EventItem();
            foreach (WindowsSessionGetEvents.Output evt in events)
            {
                writer.Write(evt.SequenceNumber);
                writer.Write(evt.SystemTime);
                writer.Write((Int64)evt.TimeStamp);
                writer.Write((Int16)evt.Event);
                writer.Write((long)evt.Pid);
                writer.Write((long)evt.WindowId);
                writer.Write(evt.Module);
                writer.Write(evt.Text);
                writer.Write((long)evt.X);
                writer.Write((long)evt.Y);
                writer.Write((long)evt.Width);
                writer.Write((long)evt.Height);
            }
        }

        public void WriteCdfFramesetHeader(BinaryWriter writer)
        {
            // TBD: We need a stored procedure to give us the # of framesets, so we can
            //  show a progress indication as we're streaming them out.
            m_framesetHeaderWriterPosition = writer.BaseStream.Position;
            writer.Write((int)0);  // Temporary till we can come back and fill it in
            m_framesetCount = 0;
        }

        public IEnumerable<double> WriteCdfFrameset(BinaryWriter writer)
        {
            foreach (var auditDatabaseId in DatabaseHelper.AuditStoreDatabaseIds)
            {
                Thread.Sleep(0);
                foreach (WindowsSessionGetFrameSets.Output output in WindowsSessionGetFrameSets.Execute(
                    m_connectionString,
                    new WindowsSessionGetFrameSets.Input(SessionId, auditDatabaseId, 0),
                    System.Data.CommandBehavior.SequentialAccess))
                {
                    Thread.Sleep(0);
                    writer.Write(m_framesetCount++);
                    writer.Write(output.SystemTime);
                    writer.Write(output.TimeStamp);
                    SqlBinary sb = output.Data.ToSqlBinary();
                    writer.Write(sb.Length);
                    writer.Write(sb.Value);

                    double progress = GetElapsedTime(output.TimeStamp).TotalMilliseconds / Length;
                    yield return progress;
                }
            }
        }

        public void WriteCdfFinish(BinaryWriter writer)
        {
            writer.Seek((int)m_framesetHeaderWriterPosition, SeekOrigin.Begin);
            writer.Write(m_framesetCount);
            writer.Flush();
        }

        public void SyncExportCdf(string filename)
        {
            Log.Verbose("Filename: {0}", filename);
            if (File.Exists(filename))
            {
                File.Delete(filename);
                Log.Verbose("Existing {0} is deleted", filename);
            }

            using (FileStream cdfFile = File.Create(filename))
            {
                BinaryWriter writer = new BinaryWriter(cdfFile);
                WriteCdfHeader(writer);
                WriteCdfEvents(writer);
                WriteCdfFramesetHeader(writer);

                foreach (double progress in WriteCdfFrameset(writer))
                {
                    //m_exportCdfProgressUpdated.Invoke(this, new ExportProgressUpdatedEventArgs(progress));
                }

                WriteCdfFinish(writer);
            }
        }

        public event EventHandler<CompletedEventArgs> ExportCdfCompleted;

        private bool m_exportingCdf;
        public bool ExportingCDF
        {
            get { return m_exportingCdf; }
            set
            {
                if (m_exportingCdf != value)
                {
                    m_exportingCdf = value;
                    OnPropertyChanged("ExportingCDF");
                }
            }
        }
    }
}
